<?php
###########################################################################
#  Copyright (C) 2006-2007 Glyphix, Inc. <briz@glyphix.com>               #
#                                                                         #
#  Permission is hereby granted, free of charge, to any person obtaining  #
#  a copy of this software and associated documentation files (the        #
#  "Software"), to deal in the Software without restriction, including    #
#  without limitation the rights to use, copy, modify, merge, publish,    #
#  distribute, sublicense, and/or sell copies of the Software, and to     #
#  permit persons to whom the Software is furnished to do so, subject to  #
#  the following conditions:                                              #
#                                                                         #
#  The above copyright notice and this permission notice shall be         #
#  included in all copies or substantial portions of the Software.        #
#                                                                         #
#  Except as contained in this notice, the name(s) of the above           #
#  copyright holders shall not be used in advertising or otherwise to     #
#  promote the sale, use or other dealings in this Software without       #
#  prior written authorization.                                           #
#                                                                         #
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        #
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     #
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
#  IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR      #
#  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,  #
#  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#  OTHER DEALINGS IN THE SOFTWARE.                                        #
###########################################################################

/**
 * GXDOM
 *
 * A general purpose XML processing library.
 *
 * Its primary purpose is to build XML documents from any data source (object, array, string, file) in very few high-level commands.
 * It also adds some options to xpath operations and allows continued GXDOM-style editing of an XML document based on the return from an xpath.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		1.0
 * @package		GXPage
 * @subpackage	Engine
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 */

/**
 * Constructor
 *
 * When creating XML elements, use the name of the element you'd like as the function name.
 * If you want to treat a function call as a real function call, add GXDOM::IS_CALL as one of the parameters.
 * See the example below for how to generate XML from the document.
 *
 * Argument order is highly important.
 * All arguments which are not constants must appear before any constants.
 *
 * @param object $doc the top-level DOMDocument element
 * @param mixed $tag the textual name of the element or an existing "me" object (see GXDOMTools::xpath)
 * @param mixed $value the actual value to be created; array, object, boolean, string
 * @param array $attributes an associative array of attributes to be added to the created element
 * @param object $to the DOMElement to which the new element will be attached
 * @param boolean $container whether to create a containing element for supplied objects or arrays
 * @param boolean $wrap when true, adds the supplied DOMElement to itself and returns itself (used in GXDOMLocal::xpath)
 * @return mixed itself or false on failure
 */
class GXDOM {

	// the 3 supported engines; used in xpath
	const AS_GXDOM			= 'GXDOM';
	const AS_SIMPLEXML		= 'SIMPLEXML';
	const AS_DOM			= 'DOM';
	// don't create a containing node for arrays and objects
	const NO_CONTAINER		= 'DO_NOT_CREATE_CONTAINER';
	// replace the calling node with the new node once created
	const REPLACE_CALLER	= 'REPLACE_CALLER';

	// reference to the xml doc
	public $doc;
	// reference to this dom element
	public $node;
	// reference to the node we're going to use as our parent
	public $to;

	// the exec object
	public $exec;

	public function __construct( $doc = null, $tag = null, $value = null, $attributes = null, &$to = null, $container = true, $wrap = false ){
		// reference document and local
		$this->exec = new GXDOMExec($this);

		// if this is the initial GXDOM instantiation, we won't have an object for $doc
			// create the document
			// store references
			// handle whatever we received inside $doc
		if( (is_string($doc) || (is_object($doc) && get_class($doc) != 'DOMDocument')) && !$wrap ){
			// create the DOM document
			$this->doc = new DOMDocument( $this->exec->getVersion(), $this->exec->getEncoding() );
			$this->doc->preserveWhiteSpace  = false;
			$this->doc->formatOutput        = true;
			$this->doc->standalone          = true;
			$this->doc->XMLStandalone       = true;
			$this->doc->substituteEntities  = true;

			// if we're here, our arguments are out of order, so straighten them out
			$attributes = $value;
			$value = $tag;
			$tag = $doc;

			// handle whatever we received in $doc
			switch( strtolower(gettype($doc)) ){
				// if doc is null, return self right away
				case 'null':
					return $this;
					break;

				//if doc is a string, treat it as the root node
				case 'string':
					// attempt to load ... if it returns false, $value is not one of the special cases load knows how to handle
					// only send $value if it's a string
					if( $this->node = $this->exec->load($tag) ){
						$this->to = $this->doc->documentElement;
						return $this;
					}
					break;

				// if array, use the default for the root node and set the value to $doc
				case 'array':
					$tag = $this->exec->getDefaultTag();
					$attributes = $value;
					$value = $doc;
					break;

				// if $doc is actually an object (but not a DOMDocument), move it to value
				case 'object':
					$tag = get_class($doc);
					$attributes = $value;
					$value = $doc;
					break;
			}
			// re-assign $doc if we messed with it in is_string
			$doc = $this->doc;
			// continue processing if we made it this far
		}

		// assign references
		$this->doc =& $doc;
		// use the specified "to" value; fall back to "doc"
		$this->to = $to;
		if( !$this->to ){
			// suppress error ... if documentElement doesn't exist, php will throw an E_STRICT error
			$this->to = (isset($this->doc->documentElement) && @$this->doc->documentElement) ? $this->doc->documentElement : $this->doc;
		}

		// if xpath was used and returned in SimpleXML format, convert the document back to DOM
		if( is_object($this->doc) && is_numeric(strpos(get_class($this->doc), 'SimpleXML')) ){
			$this->doc = dom_import_simplexml($this->doc);
		}

		// when wrapping, handle the references and return
		if( $wrap && is_bool($wrap) ){
			// when receiving a WRAP command, "node" arrives in the "tag" spot; see GXDOMLocal::xpath
			$this->node = $tag;
			// if doc still not defined, use tag
			if( !$this->doc ){
				$this->doc =& $tag;
			}
			return $this;
		}

		// determine the name of the enclosing element
		$tag = (is_string($tag)) ? $tag : $this->exec->getDefaultTag();

		// sanitize name
		$nodeName = $this->exec->cleanTag($tag);
		$bucket = $this;

		// turn booleans into strings
		if( is_bool($value) ){
			$value = ($value) ? (string)'1' : (string)'0';
		}
		// turn numbers into strings
		if( is_numeric($value) ){
			$value = (string)$value;
		}
		// create the container for this object unless directed not to
		if( $value && !is_scalar($value) && ($container || $this->to->nodeName == '#document') ){
			// attach this new node to "to", not to "node" without a value (since it's a container)
			$bucket = $this->$nodeName( null, $attributes, $this->to );
			// get rid of any attributes so they don't appear on child nodes
			$attributes = null;
			// assign the attach node to the new node
			$this->to = $bucket->node;
		}
		// if we have an empty array, make it null to avoid errors below
		if( is_array($value) && !count($value) ){
			$value = null;
		}

		// handle different types of value
		switch( gettype($value) ){

			case 'object':
				$in = null;
				$import = null;
				// if we're attaching GXDOM stuff...
				switch(true){
					// assign what to import and then continue
					case get_class($value) == 'GXDOM':
						// use "node" by default
						$import = ($value->node) ? $value->node : $value->doc->documentElement;
						break;
					// if it's simplexml, convert it to DOM and then continue
					case is_numeric(strpos(get_class($value),'SimpleXML')):
						$import = dom_import_simplexml($value);
						break;
					// import whatever we have
					case get_class($value) == 'DOMDocument' || get_class($value) == 'DOMElement':
						$import = $value;
						break;
				}

				// if we have something to import
				if( $import ){
					// if we only have the document definition, grab the top-level document element
					if( $import->nodeName == '#document' ){
						$import = $value->doc->documentElement;
					}
					try{
						/*
						if( is_null($import) ){
							trigger_error( 'import is null, returning false', E_USER_ERROR );
							return false;
						}
						*/
						$in = $this->doc->importNode( $import, true );
					}catch( DOMException $e ){
						trigger_error( 'Error importing ' .$import->nodeName .': ' .print_r($e,true), E_USER_ERROR );
						return false;
					}
					// add the imported item to the document
					$new = $this->to->appendChild($in);
					// if we made a container, set "node" to the container's node, otherwise set it to the imported node
					$this->node = $bucket->node;
					if( !$container ){
						$this->node = $new;
					}
					// set "to" to the imported node
					$this->to = $new;
					return $this;
				}

				// if we're here, we did not have a document to import, so convert it to an array and keep going
				$value = get_object_vars($value);
				// no break so goes directly to 'array'

			case 'array':
				// run through the array, adding items to bucket
				foreach( $value as $key => $val ){
					// create a container by default
					//$itemcontainer = true;
					// if the key is numeric, use the provided tag name, since tags cannot be numbers
					if( is_numeric($key) ){
						$key = ($nodeName) ? $nodeName : $bucket->to->nodeName;
						// if there's no root node, use the default tag name
						if( $key == '#document' ){
							$key = $this->exec->getDefaultTag();
						}
						// make sure to turn off container
						//$itemcontainer = GXDOM::NO_CONTAINER;
					}
					// handle boolean value ... without this, "true" was ending up null
					if( is_bool($val) ){
						$val = ($val) ? '1' : '0';
					}
					// handle string, integer, etc.
					if( !is_object($val) && !is_array($val) ){
						$val = (string)$val;
					}
					// use the node created above but attach it to this object's "to"
					// if val is not a string, create a container
					$tmp = $bucket->$key( $val, $attributes, $this->to, false );
					// return the enclosing node
					$this->node = $bucket->to;
				}
				// restore the original "node" and "to" after iterating through the array
				$this->to = $bucket->to;
				$this->node = $bucket->node;
				break;

			default:
				// create the node
				$new = null;
				$value = $this->exec->cleanText($value);
				// if value has xml reserved characters, attempt to load it as xml
				if( is_numeric(strpos($value,'<')) || is_numeric(strpos($value,'&')) ){
					// create the holding node without a value
					$new = $this->doc->createElement($nodeName);
					$frag = $this->doc->createDocumentFragment();
					// try to load a fragment, and if THAT won't load, add it as a CDATA element
					// appendXML() does not appear to throw a DOMException, so it can't be captured with a try
					if(strpos($value, '<') > -1 || strpos($value, '&') > -1){
						//trigger_error( 'XML fragment contains reserved characters, creating CDATA for "' .substr($value, 0, 100) .'..."', E_USER_WARNING );
						// override the definition of frag with a cdata element
						$frag = $this->doc->createCDATASection($value);
					}else{
						if( !@$frag->appendXML($value) ){
							trigger_error( 'Unable to parse XML fragment, creating CDATA for "' .substr($value, 0, 100) .'..."', E_USER_WARNING );
							// override the definition of frag with a cdata element
							$frag = $this->doc->createCDATASection($value);
						}
					}
					/*
					if( !@$frag->appendXML($value) ){
						trigger_error( 'Unable to parse XML fragment, creating CDATA for "' .substr($value, 0, 100) .'..."', E_USER_WARNING );
						// override the definition of frag with a cdata element
						$frag = $this->doc->createCDATASection($value);
					}
					*/
					try{
						@$new->appendChild($frag);
					}catch( DOMException $e ){
						trigger_error( 'Unable to add fragment to document: ' .print_r($e,true), E_USER_ERROR );
					}
				}else{
					try{
						$new = $this->doc->createElement( $nodeName, $value );
					}catch( DOMException $e ){
						trigger_error( 'Unable to create element (' .$nodeName .'): ' .print_r($e,true), E_USER_ERROR );
						return false;
					}
				}

				// now add the attributes if we have them
				if( is_array($attributes) ){
					foreach( $attributes as $key => $val ){
						// attribute values must not contain a less-than character '<'
						$new->setAttribute( $this->exec->cleanTag($key), str_replace('<','_',$this->exec->cleanText($val)) );
					}
				}

				// if we have no value and this node has no child nodes, get rid of the dom-created empty child
				if( !strlen($value) && $new->hasChildNodes() ){
					$new->removeChild( $new->firstChild );
				}

				$this->to->appendChild($new);
				// assign latest reference
				$this->node = $new;

		}
		// return this GXDOM
		return $this;
	}

	/**
	 * __get
	 *
	 * Calls the underlying DOMElement with the provided name.
	 * Common uses are "nodeName", "nodeValue", and "attributes".
	 * DOMElement doesn't support getAttributes(), so "attributes" is a special case requiring conversion to SimpleXML.
	 * @param string $name the name of the DOM attribute
	 * @return mixed whatever DOMElement returns
	 */
	public function __get( $name = null ){
		// if we're being asked for attributes, generate an associative array of them
		if( $name == 'attributes' ){
			return $this->__attributes();
		}
		if( isset($this->node->$name) ){
			return $this->node->$name;
		}
	}

	/**
	 * __set
	 *
	 * Calls the underlying DOMElement with the provided name and value.
	 * Cannot set attributes this way.
	 * @param string $name the name of the DOM function
	 * @param string $value the value to send to the DOM function
	 * @return mixed whatever DOMElement returns
	 */
	public function __set( $name = null, $value = null ){
		if( $name == 'attributes' ){
			return $this->__attributes($name,$value);
		}else{
			return $this->node->$name = $value;
		}
	}

	/**
	 * __attributes
	 *
	 * Handles retrieving, setting or creating attributes.
	 * @param string $name
	 * @param string $value
	 * @return DOMAttr
	 */
	public function __attributes( $name = null, $value = null ){
		// if we have an attribute node, skip it
		if( get_class($this->node) == 'DOMAttr' ){
			if( $name ){
				return $this->node->set_value($name);
			}
			return $this->node->value;
		}
		// if there's no name or value, return all attributes as an associative array
		if( !$name && !$value ){
			$att = null;
			$attributes = $this->node->attributes;
			if( !is_null($attributes) ){
				foreach( $attributes as $i => $anode ){
					$att[ $anode->name ] = $anode->value;
				}
			}
			return $att;
		}
		// if $name is a string and we have no value
		if( !strlen($value) && $name && is_string($name) ){
			return $this->node->getAttribute($name);
		}
		// if there's no value but $name is an array, we're going to create an attribute for each array item
		if( !$value && is_array($name) && count($name) ){
			foreach( $name as $key => $val ){
				if( $this->node->getAttribute($key) ){
					$this->node->setAttribute($key,$val);
				}else{
					$this->node->setAttributeNode( new DOMAttr($key,$val) );
				}
			}
			return $this->node->attributes;
		}
		// if there's already an attribute of this name, set its value
		if( $this->node->hasAttribute($name) ){
			return $this->node->setAttribute($name,$value);
		}
		// otherwise, create an attribute and attach it to this node
		return $this->node->setAttributeNode( new DOMAttr($name,$value) );
	}

	/**
	 * __call
	 *
	 * Treats the value of @method as a tag name.
	 *
	 * @param string $method the name of the method called (handled by PHP)
	 * @param array $args arguments passed to this method (handled by PHP)
	 * @return mixed GXDOM on success, false on failure
	 */
	public function __call( $method, $args ){
		if( $method != __CLASS__ ){
			// map args to names
			$argnames = array('value','attributes','to');
			$value = null;
			$attributes = null;
			$to = null;
			// constants
			$container = true;
			$replace = false;
			// loop thru the args, assigning variables as long as the value is not a constant
			foreach( $args as $a ){
				switch(true){
					// do not create a container to hold all the array or object children
					case $a === GXDOM::NO_CONTAINER:
						$container = false;
						continue;
						break;
					// replace the calling node with the to-be-created container
					case $a === GXDOM::REPLACE_CALLER:
						$replace = true;
						continue;
						break;
					// if we're here, the arg is one of the variables we need
					default:
						$name = array_shift($argnames);
						${$name} = $a;
				}
			}
			if( !isset($to) ){
				$to = $this->node;
			}
			// create the new element
			$new = new GXDOM( $this->doc, $method, $value, $attributes, $to, $container, false );
			if( $replace ){
				// get the parent node of the caller, or the document element if there is no parent
				$parent = ($this->node->parentNode) ? $this->node->parentNode : $this->doc->documentElement;
				$test = $parent->replaceChild($new->node,$this->node);
				trigger_error( 'Replaced "' .$test->nodeName .'" with "' .$new->node->nodeName .'"', E_USER_WARNING );
			}
			// return the new element
			return $new;
		}
		trigger_error( 'Invalid method call in GXDOM (' .$method .'), cannot __call self', E_USER_ERROR );
		return false;
	}

}

/**
 * GXDOMDocument
 *
 * GXDOM embeds GXDOMExec in all instantiated GXDOM objects.
 * All methods in GXDOMExec become available from GXDOM::exec.
 * Only GXDOM instantiates this object.
 */
class GXDOMExec {

	// references the calling GXDOM object
	public $gxdom;

	// the default tag name
	private $defaultTag = 'root';

	// version
	private $version = '1.0';
	// encoding
	private $encoding = 'UTF-8';

	/**
	 * __construct
	 *
	 * Receives a reference to the local GXDOM object when instantiated.
	 * Stores a reference to the calling GXDOM object, but references gxdom->doc only (not gxdom->node).
	 * @param object $gxdom
	 * @return boolean
	 */
	public function __construct( &$gxdom = null ){
		if( $gxdom ){
			$this->gxdom = $gxdom;
		}
		return true;
	}

	/**
	 * setStyleSheet
	 *
	 * Adds the xml-stylesheet processing instruction at the top of this document.
	 * @param string relative path to the XSL file
	 * @return boolean
	 */
	public function styleSheet( $href = null ){
		if( !$href ){
			trigger_error( 'Missing information: href must be a string', E_USER_ERROR );
			return false;
		}
		$elm = $this->gxdom->doc->createProcessingInstruction( 'xml-stylesheet', 'href="' .$href .'" type="text/xsl"' );
		$this->gxdom->doc->insertBefore( $elm, $this->gxdom->doc->documentElement );
		return true;
	}

	/**
	 * tag
	 *
	 * Returns a string valid for use as a node name or attribute name.
	 * @param string $name value to sanitize
	 * @return string valid name
	 */
	public function cleanTag( $name = null ){
		if( !$name ){
			return false;
		}
		// If it's not a valid node name, sanitize it.
		$from = array(' ','@','!','&','<','>','/','\\','%','$','(',')','{','}','#');
		$tmp = trim( str_replace($from,'_',$name) );
		// If trimming it results in nothing, call it entry.
		if( !$tmp ){
			$tmp = 'entry';
		}
		// Names must begin with a letter. If it doesn't, prepend it with an underscore.
		if( preg_match( "/[^a-z]+/i", substr($tmp,0,1) ) && substr($tmp,0,1) != '_' ){
			trigger_error( 'Key name (' .$name .') is not valid, returning _' .$tmp .' / ', E_USER_ERROR );
			$tmp = '_' .$tmp;
		}
		return $tmp;
	}

	/**
	 * escapeText
	 *
	 * Returns values suitable for inclusion in a UTF-8 document.
	 * No other encoding values are supported.
	 * This will take all entities such as &nbsp; and replace them with their actual ascii values.
	 *
	 * @param string $str value to be escaped
	 * @return string escaped value
	 */
	public function cleanText( $str = null ){
		if( !strlen($str) ){
			return false;
		}
		// finds all html entities (&nbsp;) and turns them into the actual character they represent
		return html_entity_decode( $str, ENT_QUOTES, 'UTF-8' );
	}

	/**
	 * getDefaultTag
	 *
	 * Returns the current default tag name.
	 * Users can change it at any time.
	 * @return string
	 */
	public function getDefaultTag(){
		return $this->defaultTag;
	}

	/**
	 * setDefaultTag
	 *
	 * Sets the default tag name for this instance only.
	 * @param string $name the name of the desired default tag.
	 * @return boolean
	 */
	public function setDefaultTag( $name = null ){
		$this->gxdom->defaultTag = $name;
		return true;
	}

	/**
	 * getEncoding
	 *
	 * Returns the current encoding value.
	 * Users can change it at any time.
	 * @return string
	 */
	public function getEncoding(){
		return $this->encoding;
	}

	/**
	 * setEncoding
	 *
	 * Sets the encoding of the top-level DOMDocument of the referenced GXDOM object.
	 * Users can change it at any time.
	 * @param string a valid encoding value
	 * @return boolean
	 */
	public function setEncoding( $str = 'UTF-8' ){
		$this->encoding = $str;
		$this->gxdom->doc->encoding = $str;
		trigger_error( 'Set encoding of ' .$this->gxdom->doc->documentElement->nodeName .' to ' .$str, E_USER_WARNING );
		return true;
	}

	/**
	 * getVersion
	 *
	 * Returns the current XML version.
	 * @return string
	 */
	public function getVersion(){
		return $this->version;
	}

	/**
	 * get
	 *
	 * Returns the underlying DOM or SimpleXML object referenced by this object in the specified engine.
	 * @param constant $engine the engine in which to return the underlying node
	 * @return object
	 */
	public function get( $engine = GXDOM::AS_SIMPLEXML ){
		$obj = ($this->gxdom->node) ? $this->gxdom->node : $this->gxdom->doc->documentElement;
		if( $engine === GXDOM::AS_SIMPLEXML ){
			$obj = simplexml_import_dom(clone $obj);
		}
		return $obj;
	}

	/**
     * xml
     *
     * Generates the XML string for the current $doc or local node.
     * @return string
     */
	public function xml(){
		$doc = null;
		// determine if this instance has a local node
		// if so, attempt to generate xml just for this local node, which requires simplexml
		if( isset($this->gxdom->node) && is_object($this->gxdom->node) ){
			$doc = simplexml_import_dom(clone $this->gxdom->node);
		}else{
			$doc = $this->gxdom->doc;
		}

		// now generate the xml
		switch( get_class($doc) ){
			case 'DOMDocument':
				$str = $doc->saveXML();
				break;
			case 'SimpleXMLElement':
				$str = $doc->asXML();
				break;
			default:
				trigger_error( 'Unable to determine the document type (' .get_class($doc) .')', E_USER_ERROR );
				return false;
		}

		return $str;
	}

	/**
	 * xpath
	 *
	 * Runs the supplied xpath on the supplied node and returns the results.
	 * Scope is the current node if it exists, the document otherwise.
	 *
	 * It defaults to returning the results in GXDOM format.
	 * GXDOM::AS_DOM forces the return to be in DOM format, and GXDOM::AS_SIMPLEXML returns in SimpleXML format.
	 *
	 * $index returns the result specified by the index.
	 * In essence, $index is nth-child of the results.
	 *
	 * @param string $xpath the xpath to be run on this object's referenced object
	 * @param integer $index returns the element specified by $index (default is all results)
	 * @param constant $engine determines the format of the returned results (GXDOM, DOM, SimpleXML)
	 * @param array $namespace a numerically-indexed array containing [prefix,uri]
	 * @return mixed object, array or false on failure
	 */
	//public function xpath( $xpath = null, $index = null, $engine = GXDOM::AS_GXDOM, $namespace = null ){
	public function xpath(){
		// grab the arguments and rejigger them
		$args = func_get_args();
		$argnames = array('xpath','index');
		$xpath = null;
		$index = null;
		$engine = GXDOM::AS_GXDOM;
		$namespace = array();
		foreach( $args as $a ){
			switch(true){
				case $a === GXDOM::AS_GXDOM || $a === GXDOM::AS_SIMPLEXML || $a === GXDOM::AS_DOM:
					$engine = $a;
					continue;
					break;
				case is_array($a):
					$namespace = $a;
					continue;
					break;
				// if we're here, the arg is one of the variables we need
				default:
					$name = array_shift($argnames);
					${$name} = $a;
			}
		}

		// must have an xpath
		if( !is_string($xpath) ){
			trigger_error( 'Missing xpath', E_USER_ERROR );
			return false;
		}

		// ensure we have an object to work with
		$doc = $this->gxdom->doc;
		$context = (is_object($this->gxdom->node)) ? $this->gxdom->node : $this->gxdom->doc;
		// we're using DOM::evaluate, which returns a single typed result if possible, so don't deal with index if we get one
		$processIndex = true;

		// handle various engines
		switch( $engine ){

			case 'GXDOM':
			case 'DOM':
				$x = new DOMXPath( $this->gxdom->doc );
				// if we have a namespace
				if( $namespace ){
					// grab the first key ... if it's numeric, assume we have a numerically-indexed array with a single namespace
					$keys = array_keys($namespace);
					$space = array_pop($keys);
					if(is_numeric($space)){
						trigger_error('xpath: registering namespace [' .$namespace[0] .'=' .$namespace[1] .']', E_USER_WARNING);
						$x->registerNamespace($namespace[0], $namespace[1]);
					}else{
						foreach($namespace as $nsname => $nsuri){
							trigger_error('xpath: registering namespace [' .$nsname .'=' .$nsuri .']', E_USER_WARNING);
							$x->registerNamespace($nsname, $nsuri);
						}
					}
				}
				try{
					// evaluate returns a typed result if possible (meaning, there's only one item returned)
					$xresult = @$x->evaluate( $xpath, $context );
				}catch( Exception $e ){
					trigger_error( 'Unable to execute xpath, or invalid xpath query string (' .$xpath .')', E_USER_ERROR );
					return false;
				}
				// if we didn't get a list, make sure we don't try to get one of the items
				if( ! is_object($xresult) || (is_object($xresult) && get_class($xresult) != 'DOMNodeList') ){
					$processIndex = false;
				}
				// if we have something but it's not an object, just return it
				if( $xresult && !is_object($xresult) ){
					return $xresult;
				}
				// if no results, return false
				if( isset($xresult->length) && $xresult->length == 0 ){
					trigger_error( 'xpath (' .$xpath .') had no results', E_USER_WARNING );
					return false;
				}
				break;

			default:
				$this->gxdom->doc = simplexml_import_dom(clone $this->gxdom->doc->documentElement);
				$context = simplexml_import_dom(clone $context);
				try{
					$xresult = $context->xpath( $xpath );
				}catch(Exception $e){
					trigger_error( 'Unable to execute xpath, or invalid xpath query string (' .$xpath .')', E_USER_ERROR );
					return false;
				}
				if( !count($xresult) ){
					trigger_error( 'xpath (' .$xpath .') had no results', E_USER_WARNING );
					return false;
				}

		}

		// handle the result
		if( is_numeric($index) && $processIndex ){
			switch($engine){
				case 'GXDOM':
					$item = $xresult->item($index);
					// use "to" if it's defined; fall back to null to force GXDOM to figure it out
					$to = (isset($this->gxdom->to)) ? $this->gxdom->to : $item;
					$xresult = new GXDOM( $doc, $item, null, null, $to, false, true );
					break;
				case 'SIMPLEXML':
					$xresult = $xresult[$index];
					break;
				case 'DOM':
					$xresult = $xresult->item($index);
					break;
				default:
					trigger_error( 'Invalid returning value specified (' .$engine .')', E_USER_WARNING );
					return false;
			}
		}else{
			// if we're returning GXDOM, wrap all returned elements with GXDOM
			if( $engine == 'GXDOM' ){
				$r = null;
				// can't change a DOMNodeList directly, have to build a new array
				$i = 0;
				foreach( $xresult as $item ){
					$to = $xresult->item($i);
					$r[] = new GXDOM( $doc, $xresult->item($i), null, null, $to, false, true );
					$i++;
				}
				$xresult = $r;
			}
		}
		return $xresult;
	}

	/**
	 * load
	 *
	 * Loads a file, HTML or XML, and automatically determines which to do by examining the content of the provided string.
	 * @param string $str the string to load
	 * @return mixed object on success, false on failure
	 */
	public function load( $str = null, $replace = false ){
		// str must be a scalar value to be loaded
		if( !$str || !is_scalar($str) ){
			trigger_error( 'Missing string or value is not scalar', E_USER_ERROR );
			return false;
		}

		// determine what to attach to
		$DOMnode = (isset($this->gxdom->node)) ? $this->gxdom->node : (($this->gxdom->doc->documentElement) ? $this->gxdom->doc->documentElement : $this->gxdom->doc);

		// figure out how to handle the provided string
		switch(true){

			// if it's a file
			case is_string($str) && strlen($str) < 4096 && is_file($str):
				// load the file
				$newdoc = new DOMDocument();
				try{
					$newdoc->load($str);
				}catch( DOMException $e ){
					trigger_error( 'Unable to load XML file ... verify that ' .basename($str) .' is well-formed', E_USER_NOTICE );
					trigger_error( 'Error loading XML file: ' .print_r($e,true), E_USER_ERROR );
					return false;
				}
				$import = $this->gxdom->doc->importNode($newdoc->documentElement,true);
				if( $replace ){
					$parent = ($DOMnode->parentNode) ? $DOMnode->parentNode : $DOMnode;
					$test = $parent->replaceChild($import,$DOMnode);
					$DOMnode = $import;
				}else{
					$DOMnode->appendChild($import);
				}
				break;

			// if it looks like html
			case is_numeric(strpos($str,'</html>')) || is_numeric(strpos($str,'</HTML>')):
				try{
					$newdoc = @DOMDocument::loadHTML($str);
					$import = $this->gxdom->doc->importNode($newdoc->documentElement,true);
					$DOMnode->appendChild($import);
				}catch( DOMException $e ){
					trigger_error( 'Unable to load HTML (' .$str .')', E_USER_ERROR );
					return false;
				}
				break;

			// if it looks like xml
			case substr(trim($str),0,1) == '<' && substr(trim($str),strlen(trim($str))-1,1) == '>':
				try{
					if( get_class($DOMnode) == 'DOMDocument' ){
						$DOMnode->loadXML($str);
					}else{
						$frag = $this->gxdom->doc->createDocumentFragment();
						try{
							$frag->appendXML( $this->cleanText($str) );
							$DOMnode->appendChild( $frag );
						}catch(DOMException $e){
							unset($frag);
							trigger_error( 'Unable to parse XML fragment, creating CDATA: ' .print_r($e,true), E_USER_WARNING );
							$DOMnode->appendChild( $this->gxdom->doc->createCDATASection( $this->cleanText($str) ) );
						}
					}
				}catch(Exception $e){
					trigger_error( 'Unable to load XML (' .$str .'): ' .print_r($e,true), E_USER_ERROR );
					return false;
				}
				break;

			// if we can't handle whatever is in the string, return false
			default:
				return false;
		}

		return $DOMnode;
	}

}
